home *** CD-ROM | disk | FTP | other *** search
/ Nebula 1 / Nebula One.iso / Utilities / Other / mCD / Source / mCD_Controller.m < prev    next >
Encoding:
Text File  |  1995-03-19  |  34.1 KB  |  1,117 lines

  1.  
  2. #import "mCD_Controller.h"
  3. #import "PrefsController.h"
  4. #import "CD_DBase.subproj/CD_DBase.h"
  5.  
  6. void fixCatNum(struct rsc_media_catnum_reply *);
  7.  
  8. @implementation mCD_Controller
  9.  
  10. /* implement a periodic update */
  11. void Trigger(DPSTimedEntry teNum, double new, id target)
  12. {
  13.     [target EventLoop];
  14. }
  15.  
  16. - EventLoopInit
  17. {
  18.     teNum = DPSAddTimedEntry(updatePeriod, (DPSTimedEntryProc)Trigger,
  19.                     self, NX_BASETHRESHOLD);
  20.     return self;
  21. }
  22.  
  23. - EventLoop
  24. {
  25.     int  devReady;
  26.     struct timeval    timed_cd_Tval;
  27.  
  28.     if ( (cd_fd != 0) && !do_timed_updates ) {
  29.     devReady = do_testunitready(cd_fd, &timed_cd_Tval, &tur_Ereply);
  30.     if ( !devReady ) {
  31.         /* Hmm, will this do what I want? */
  32.         [self loadCD:self];
  33.         }
  34.     }
  35.  
  36.     if ( do_timed_updates ) [self updateCdStatus:self];
  37.     return self;
  38. }
  39.  
  40. - appDidInit:sender
  41. {
  42.     if ( 0 != geteuid() ) {
  43.     id    nonRootErrorPanel;
  44.     nonRootErrorPanel = [NXApp loadNibSection:"NonRoot.nib" owner:NXApp];
  45.     [NXApp runModalFor:nonRootErrorPanel];
  46.     exit(0);
  47.     }
  48.  
  49.     [globPrefs initGlobalPreferences:self mainPanel:mainPanel ];
  50.  
  51.     /* NOTE: in NS-3.2, it seems that setAltIncrementValue will work
  52.      *       backwards, at least for vertical sliders.  I still want
  53.      *       to have it though, it's better backwards than not at all
  54.      */
  55.     [leftVolumeSliderID  setAltIncrementValue: 1.0];
  56.     [rightVolumeSliderID setAltIncrementValue: 1.0];
  57.     [pauseButtonID setShowsStateBy:NX_CONTENTS | NX_CHANGEBACKGROUND];
  58.     
  59.     cd_fd = 0;
  60.     [unloadButtonID setEnabled:NO];
  61.  
  62.     [curTrackID setStringValue: "--"];
  63.     [endTrackID setStringValue: "--"];
  64.     [trackPlayTimeID setStringValue: "-:--:--"];
  65.     [trackRemTimeID  setStringValue: "-:--:--"];
  66.     [discPlayTimeID setStringValue: "-:--:--"];
  67.     [discRemTimeID  setStringValue: "-:--:--"];
  68.     [trackInfoID selectAll:self];
  69.     [trackInfoID replaceSel: ""];
  70.     [mainPanel makeKeyAndOrderFront:self];
  71.  
  72.     /* start up the periodic update of the cd status */
  73.     do_timed_updates = NO;    /* initial testing */
  74.     updatePeriod = 1.0;        /* every 1 second */
  75.     updatePeriod = 0.5;        /* crazy times for testing purposes */
  76.     updatePeriod = 0.99;    /* a little less crazy... */
  77.     [self EventLoopInit];
  78.  
  79.     /* if the preferences are indeed set for a CD-ROM drive, then
  80.      * try to load a music CD, and if it's already in there go
  81.      * to display it.  Note that this will not (yet) automatically
  82.      * prompt to load in a CD if there isn't one there.
  83.      */
  84.     if ( [self openCdFd:FROM_CD_APP_DID_INIT] ) {
  85.     [self displayCdInfo];
  86.     }
  87.     return self;
  88. }
  89.  
  90. - appWillTerminate:sender
  91. {
  92.     /* the purpose of this method is to handle "Quit" processing */
  93.     BOOL wantUnload, abortQuit;
  94.     int  devReady, alertResult;
  95.     
  96.     if ( cd_fd == 0 ) return self;    /* no device open, nothing to do */
  97.  
  98.     /* assume an unload of the CD drive will be wanted.  If the device
  99.      * is ready (ie, there's a disc in it), and if the disc is playing,
  100.      * then prompt the user to find out of it's really wanted.  If the
  101.      * drive has no CD, or the CD isn't playing, then always do an
  102.      * unload to prevent later confusion.
  103.      */
  104.  
  105.     wantUnload = YES;            /* assume an unload will be wanted */
  106.     abortQuit = NO;            /* and the Quit won't be aborted */
  107.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  108.     if ( !devReady ) {
  109.     /* there is a disc in the drive */
  110.     if (cd_curpos.rsc_audio_status == RSC_ASTAT_PLAYING ) {
  111.         alertResult = NXRunAlertPanel("mCD Quitting",
  112.         "There is a music CD still playing.  Should it be unloaded during quit?",
  113.         "Yes", "No", "Cancel Quit", rawDevName);
  114.         switch ( alertResult ) {
  115.             case NX_ALERTALTERNATE:
  116.             wantUnload = NO;
  117.             break;
  118.         case NX_ALERTOTHER:
  119.             wantUnload = NO;
  120.             abortQuit = YES;
  121.             break;
  122.         }
  123.         }
  124.     }
  125.  
  126.     if (wantUnload) [self unloadCD:self];
  127.     else {
  128.     /* user wants the music CD left in the drive, playing, but
  129.      * we should at least enable the media-removal button (on
  130.      * the drive) to work again
  131.      */
  132.     do_preventremoval_1e(cd_fd, NO, &cd_Ereply); /* ie, allow removal */
  133.     }
  134.     
  135.     if (abortQuit) return NULL;
  136.     
  137.     return self;  /* anything non-null, so app will terminate */
  138. }
  139.  
  140. - (int)appPowerOffIn:(int)ms andSave:(int)aFlag
  141. {
  142.     /* the purpose of this method is to handle logout processing,
  143.      * which is not handled by the appWillTerminate method.  I
  144.      * don't really care about power-off processing, but this
  145.      * seems to be the only way to handle logout processing
  146.      */
  147.  
  148.     /* this method is described *very* briefly in the pre-3.0
  149.      * concepts documentation.  While it works in NS-3.x (on
  150.      * motorola hardware, at least), I suspect NeXT is phasing
  151.      * it out.  If this routine was doing any significant amount
  152.      * of work, it might need to send the Workspace manager a:
  153.     - (int)extendPowerOffBy:(int)requestedMs actual:(int *)actualMs;
  154.      * message to get more time to do the processing.
  155.      */
  156.  
  157.     /* clean up after ourselves as quickly as possible.  this is
  158.      * basically a trimmed-down version of "unload" processing
  159.      */
  160.     int err;
  161.  
  162.     if ( cd_fd == 0 ) return 0;        /* no device currently, ignore */
  163.  
  164.     do_preventremoval_1e(cd_fd, NO, &cd_Ereply); /* ie, allow removal */
  165.     do_eject_1b(cd_fd, &cd_Tval, &cd_Ereply);
  166.     err = ioctl(cd_fd, DKIOCEJECT, NULL);
  167.     close( cd_fd );
  168.     cd_fd = 0;
  169.     
  170.     return 0 ; /* not sure what this method needs to return... */
  171. }
  172.  
  173. - updateCdStatus:sender
  174. {
  175.     BOOL needUpdate, indexChanged;
  176.     int  tempHour, tempMin, tempSec;
  177.     int  lastCdIndex, lastAbsSecond;
  178.     char buff[80];
  179.  
  180.     if ( cd_fd == 0 ) return self;    /* no device yet, ignore */
  181.  
  182.     needUpdate = indexChanged = NO;
  183.  
  184.     do_readcurrentposition_42(cd_fd, &cd_curpos, &rcp_Ereply);
  185.  
  186.     switch (cd_curpos.rsc_audio_status) {
  187.     case RSC_ASTAT_PAUSED:
  188.     case RSC_ASTAT_PLAYING:
  189.         break;
  190.     case RSC_ASTAT_PLAYCOMPLETE:
  191.         if ( [globPrefs consoleDebugMsgs] )
  192.             printf("mCD debug: play complete\n");
  193.         [self stopCD:self];
  194.         break;
  195.     case RSC_ASTAT_PLAYABORTED:
  196.         if ( [globPrefs consoleDebugMsgs] )
  197.             printf("mCD debug: play aborted\n");
  198.         /* presumably not wanted: [self stopCD:self]; */
  199.         break;
  200.     case RSC_ASTAT_NONE:
  201.         break;
  202.     }
  203.  
  204.     lastCdIndex = curCdIndex;
  205.     curCdIndex = (cd_curpos.track * 100) + cd_curpos.index;
  206.     if ( lastCdIndex != curCdIndex ) needUpdate = indexChanged = YES;
  207.     
  208.     lastAbsSecond = curAbsSecond;
  209.     curAbsSecond =  (cd_curpos.abs_hour * 3600) + (cd_curpos.abs_min * 60) +
  210.                 cd_curpos.abs_sec;
  211.     if ( lastAbsSecond != curAbsSecond ) needUpdate = YES;
  212.     
  213.     if ( needUpdate ) {
  214.     [mainPanel disableFlushWindow];
  215.     
  216.     if ( cd_curpos.index < 2 ) {
  217.         sprintf (buff, "%d", cd_curpos.track);
  218.         }
  219.     else {
  220.         sprintf (buff, "%d.%d", cd_curpos.track, cd_curpos.index);
  221.         }
  222.     [curTrackID setStringValue: buff];
  223.     if ( (cd_curpos.index == 0) &&  (cd_curpos.rel_sec > 0) ) {
  224.         sprintf (buff, "- %02d:%02d",
  225.                 cd_curpos.rel_min, cd_curpos.rel_sec);
  226.         }
  227.     else {
  228.         sprintf (buff, "%d:%02d:%02d", cd_curpos.rel_hour,
  229.                 cd_curpos.rel_min, cd_curpos.rel_sec);
  230.         }
  231.     [trackPlayTimeID setStringValue: buff];
  232.     switch (cd_curpos.rsc_audio_status) {
  233.       case RSC_ASTAT_PLAYING:
  234.         /* the CD is playing right now... */
  235.         if ( (indexChanged) &&
  236.              (cd_curpos.index + cd_curpos.rel_sec > 0) &&
  237.                  [globPrefs consoleDebugMsgs] ) {
  238.             printf("mCD debug: index %d.%d started %d:%02d:%02d-%02d into CD",
  239.                 cd_curpos.track, cd_curpos.index,
  240.                 cd_curpos.abs_hour, cd_curpos.abs_min,
  241.                 cd_curpos.abs_sec, cd_curpos.abs_frame);
  242.             if ((cd_curpos.rel_hour > 0) ||
  243.                 (cd_curpos.rel_min > 0) || (cd_curpos.rel_sec > 0)) {
  244.             if ( cd_curpos.index != 0 ) {
  245.                 printf(", %d:%02d:%02d into the song",
  246.                     cd_curpos.rel_hour, cd_curpos.rel_min,
  247.                     cd_curpos.rel_sec);
  248.                 }
  249.             else {
  250.                 printf(", -%02d:%02d before the song",
  251.                     cd_curpos.rel_min, cd_curpos.rel_sec);
  252.                 }
  253.             }
  254.             printf("\n");
  255.             }
  256.         tempSec = toc.info[cd_curpos.track].elapsedSec - (
  257.                 (cd_curpos.rel_hour * 3600) +
  258.                 (cd_curpos.rel_min * 60) +
  259.                 cd_curpos.rel_sec );
  260.         tempMin = tempSec / 60;
  261.         tempSec = tempSec % 60;
  262.         tempHour = tempMin / 60;
  263.         tempMin = tempMin % 60;
  264.         sprintf (buff, "%d:%02d:%02d", tempHour, tempMin, tempSec);
  265.         [trackRemTimeID setStringValue: buff];
  266.         tempSec = toc.info[100].elapsedSec - (
  267.                 (cd_curpos.abs_hour * 3600) +
  268.                 (cd_curpos.abs_min * 60) +
  269.                 cd_curpos.abs_sec );
  270.         tempMin = tempSec / 60;
  271.         tempSec = tempSec % 60;
  272.         tempHour = tempMin / 60;
  273.         tempMin = tempMin % 60;
  274.         sprintf (buff, "%d:%02d:%02d", tempHour, tempMin, tempSec);
  275.         [discRemTimeID setStringValue: buff];
  276.         break;
  277.     }
  278.     sprintf (buff, "%d:%02d:%02d", cd_curpos.abs_hour,
  279.             cd_curpos.abs_min, cd_curpos.abs_sec);
  280.     [discPlayTimeID setStringValue: buff];
  281.     [mainPanel reenableFlushWindow];
  282.     [mainPanel flushWindowIfNeeded];
  283.     }
  284.     
  285.     return self;
  286. }
  287.  
  288. - ejectCD:sender
  289. {
  290.     int  devReady;
  291.     char buff[80];
  292.  
  293.     if ( cd_fd == 0 ) return self;    /* no device yet, ignore */
  294.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  295.     if ( devReady )   return self;    /* device is not ready, ignore */
  296.  
  297.     do_timed_updates = NO;  /* initial testing */
  298.  
  299.     /* zap a few display fields to reduce confusion */
  300.     [trackInfoID selectAll:self];
  301.     [trackInfoID replaceSel: ""];
  302.  
  303.     [curTrackID setStringValue: "--"];
  304.     [endTrackID setIntValue: toc.lasttrack];
  305.     [trackPlayTimeID setStringValue: "-:--:--"];
  306.     [trackRemTimeID setStringValue: "-:--:--"];
  307.     [discPlayTimeID setStringValue: "-:--:--"];
  308.     if( toc.info[100].min >= 60 ) {
  309.     toc.info[100].hour++ ;
  310.     toc.info[100].min -= 60;
  311.     }
  312.     sprintf (buff, "%u:%02u:%02u", toc.info[100].hour,
  313.             toc.info[100].min, toc.info[100].sec);
  314.     [discRemTimeID setStringValue: buff];
  315.  
  316.     do_preventremoval_1e(cd_fd, NO, &cd_Ereply); /* ie, allow removal */
  317.     do_eject_1b(cd_fd, &cd_Tval, &cd_Ereply);
  318.  
  319.     return self;
  320. }
  321.  
  322. - unloadCD:sender
  323. {
  324.     int err;
  325.  
  326.     if ( cd_fd == 0 ) return self;    /* no device yet, ignore */
  327.     /* (note that we can do the unload processing even if there is
  328.      * no disc in the drive, so we don't testunitready here ) */
  329.  
  330.     [self ejectCD:self];
  331.  
  332.     /* the difference between ejecting and unloading is the
  333.        following commands:  */
  334.     err = ioctl(cd_fd, DKIOCEJECT, NULL);
  335.     close( cd_fd );
  336.     cd_fd = 0;
  337.  
  338.     [loadButtonID setEnabled:YES];
  339.     [unloadButtonID setEnabled:NO];
  340.     return self;
  341. }
  342.  
  343. - loadCD:sender
  344. {
  345.     if ( [self openCdFd:FROM_LOAD_CD_REQUEST] ) {
  346.     [self displayCdInfo];
  347.     }
  348.  
  349.     return self;
  350. }
  351.  
  352. - displayCdInfo
  353. {
  354.     int    track;
  355.     char buff[120];
  356.     NXStream *songInfoStream;
  357.  
  358.     [self updateCdStatus:self];
  359.     switch (cd_curpos.rsc_audio_status) {
  360.     case RSC_ASTAT_PAUSED:
  361.     case RSC_ASTAT_PLAYING:
  362.         break;
  363.     case RSC_ASTAT_PLAYCOMPLETE:
  364.     case RSC_ASTAT_PLAYABORTED:
  365.     case RSC_ASTAT_NONE:
  366.         do_spinup_1b(cd_fd, &cd_Tval, &cd_Ereply);
  367.     }
  368.     do_readtoc_43(cd_fd, &toc, &cd_Ereply);
  369.     [self fillTocTitles];
  370.     
  371.     [curTrackID setIntValue: 0];
  372.     [endTrackID setIntValue: toc.lasttrack];
  373.     switch (cd_curpos.rsc_audio_status) {
  374.     case RSC_ASTAT_PAUSED:
  375.         /* have to skip over this for now, due to the odd behavior
  376.          * of the CD-ROM drive that NeXT used to sell, which
  377.          * indicates it's paused if it's not actively playing and
  378.          * if a play operation didn't just complete.  This part
  379.          * could be done on a drive-specific basis, once the code
  380.          * is better organized.
  381.          */
  382.         /* do nothing but ensure pause button is right */
  383.         /* [pauseButtonID setIntValue:1]; */
  384.         break;
  385.     case RSC_ASTAT_PLAYING:
  386.         /* do nothing but ensure pause button is right */
  387.         [pauseButtonID setIntValue:0];
  388.         break;
  389.     case RSC_ASTAT_PLAYCOMPLETE:
  390.     case RSC_ASTAT_PLAYABORTED:
  391.     case RSC_ASTAT_NONE:
  392.         break;
  393.     }
  394.  
  395.     [trackPlayTimeID setStringValue: "-:--:--"];
  396.     [trackRemTimeID setStringValue: "-:--:--"];
  397.     if( toc.info[100].min >= 60 ) {
  398.     toc.info[100].hour++ ;
  399.     toc.info[100].min -= 60;
  400.     }
  401.     sprintf (buff, "%u:%02u:%02u", toc.info[100].hour,
  402.             toc.info[100].min, toc.info[100].sec);
  403.     [discRemTimeID setStringValue: buff];
  404.     
  405.     if ( ! toc.discTitle )
  406.         [cdTitleID setStringValue:"title of CD is not known"];
  407.     else {
  408.         tempTitle[0] = '\0';
  409.         if ( toc.discPerformer ) {
  410.             strcat(tempTitle, toc.discPerformer);
  411.             strcat(tempTitle, " - ");
  412.             }
  413.         strcat(tempTitle, toc.discTitle);
  414.         [cdTitleID setStringValue: tempTitle];
  415.         }
  416.     
  417.     /* display the table of contents to the window, as rich-text */
  418.     songInfoStream = NXOpenMemory(NULL, 0, NX_READWRITE);
  419.     NXPrintf(songInfoStream,
  420.     "{\\rtf0\\ansi{\\fonttbl\\f0\\fmodern Ohlfs;");
  421.     NXPrintf(songInfoStream, "\\f1\\fnil Times-Roman;}\n");
  422.     NXPrintf(songInfoStream, "\\pard\\tx460\\tx1060\\tx1260");
  423.     NXPrintf(songInfoStream, "\\f0\\b0\\i0\\ulnone\\fs18");
  424.     NXPrintf(songInfoStream, "\\fi-1260\\li1260\\fc0\\cf0\n");
  425.     for ( track = toc.firsttrack; track <= toc.lasttrack; track++ ) {
  426.     NXPrintf(songInfoStream,
  427.             "\\f0\\fs18%3d)\t%02d:%02d\t-\t\\f1\\fs24 ",
  428.             track,
  429.             toc.info[track].elapsedSec / 60,
  430.             toc.info[track].elapsedSec % 60);
  431.     if ( toc.info[track].trackTitle )
  432.         NXPrintf(songInfoStream, "%s", toc.info[track].trackTitle);
  433.     else    NXPrintf(songInfoStream, "???");
  434.     if ( track != toc.lasttrack ) NXPrintf(songInfoStream, "\\\n");
  435.     }
  436.     NXPrintf(songInfoStream, "\n}\n");
  437.     NXSeek(songInfoStream, 0, NX_FROMSTART);
  438.     [trackInfoID selectAll:self];
  439.     [trackInfoID replaceSelWithRichText: songInfoStream];
  440.     NXCloseMemory(songInfoStream, NX_FREEBUFFER);
  441.  
  442.     return self;
  443. }
  444.  
  445. - pauseCD:sender
  446. {
  447.     /* note that this method is called from both the pause
  448.      * button and a menu item.  Due to the menu item, this
  449.      * has to directly set the state of the button (instead
  450.      * of just sending a message to "sender").
  451.      */
  452.     do_readcurrentposition_42(cd_fd, &cd_curpos, &rcp_Ereply);
  453.     switch (cd_curpos.rsc_audio_status) {
  454.     case RSC_ASTAT_PLAYING:
  455.         /* it is playing, switch it to pause */
  456.         do_pauseaudio_4b(cd_fd, 1, &cd_Ereply);
  457.         [pauseButtonID setIntValue:1];
  458.         break;
  459.     case RSC_ASTAT_PAUSED:
  460.         /* it is paused, switch it to playing */
  461.         do_pauseaudio_4b(cd_fd, 0, &cd_Ereply);
  462.         [pauseButtonID setIntValue:0];
  463.         break;
  464.     case RSC_ASTAT_PLAYCOMPLETE:
  465.     case RSC_ASTAT_PLAYABORTED:
  466.     case RSC_ASTAT_NONE:
  467.         /* do nothing but ensure pause button is right */
  468.         [pauseButtonID setIntValue:0];
  469.         break;
  470.     }
  471.  
  472.     return self;
  473. }
  474.  
  475. - playCD:sender
  476. {
  477.     int    sTrack = 1, eTrack = toc.lasttrack;
  478.  
  479.     [self playTracks:sTrack to: eTrack];
  480.     
  481.     return self;
  482. }
  483.  
  484. - playTrackRange: sender
  485. {
  486.     /* target for the play-range button in the
  487.        select track range panel */
  488.     int    sTrack, eTrack;
  489.  
  490.     sTrack = [newStartTrackID intValue];
  491.     eTrack = [newEndTrackID intValue];
  492.     [self playTracks:sTrack to: eTrack];
  493.     [trackRangePanel performClose:self];
  494.  
  495.     return self;
  496. }
  497.  
  498. - setLeftVolume:sender
  499. {
  500.     [self setPlayVolumes:[sender intValue] :-1];
  501.     return self;
  502. }
  503.  
  504. - setRightVolume:sender
  505. {
  506.     [self setPlayVolumes:-1 :[sender intValue]];
  507.     return self;
  508. }
  509.  
  510. - setPlayVolumes:(int) leftVol :(int) rightVol
  511. {
  512.     int  devReady;
  513.  
  514.     if ( cd_fd == 0 ) return self;    /* no device yet, ignore */
  515.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  516.     if ( devReady )   return self;    /* device is not ready, ignore */
  517.     
  518.     if ( [globPrefs separateVolumes] || ((leftVol < 0) && (rightVol < 0))) {
  519.     /* need to know current volume settings */
  520.     do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
  521.     if (leftVol < 0)  leftVol = cd_volumes.ch0_vol;
  522.     if (rightVol < 0) rightVol = cd_volumes.ch1_vol;
  523.     }
  524.     else {
  525.     if (leftVol < 0) {
  526.         cd_volumes.ch0_vol = leftVol = rightVol;
  527.         [leftVolumeSliderID setIntValue:leftVol];
  528.         }
  529.     if (rightVol < 0) {
  530.         cd_volumes.ch1_vol = rightVol = leftVol;
  531.         [rightVolumeSliderID setIntValue:rightVol];
  532.         }
  533.     }
  534.     
  535.     do_modeselect_pc_E(cd_fd, leftVol, rightVol, &cd_Ereply);
  536.     return self;
  537. }
  538.  
  539. /* the following used by PrefController */
  540. - getVolumes:(int *)leftVolPtr :(int *)rightVolPtr
  541. {
  542.     do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
  543.     *leftVolPtr  = cd_volumes.ch0_vol;
  544.     *rightVolPtr = cd_volumes.ch1_vol;
  545.     return self;
  546. }
  547.  
  548. - showSelectTrackRange:sender
  549. {
  550.     [newStartTrackID setIntValue: 1];
  551.     [newEndTrackID setIntValue: toc.lasttrack];
  552.     [trackRangePanel makeKeyAndOrderFront:self];
  553.     [newStartTrackID selectText:self];
  554.     return self;
  555. }
  556.  
  557. - goNextTrack:sender
  558. {
  559.     int    sTrack, eTrack;
  560.  
  561.     if ( cd_curpos.track >= toc.lasttrack ) {
  562.     return self;
  563.     }
  564.  
  565.     sTrack = cd_curpos.track + 1;
  566.     eTrack = toc.lasttrack;  /* for now */
  567.     [self playTracks:sTrack to: eTrack];
  568.  
  569.     return self;
  570. }
  571.  
  572. - goPreviousTrack:sender
  573. {
  574.     int    sTrack, eTrack;
  575.  
  576.     /* goes to the beginning of the current track if we're more than
  577.        a few seconds into it, otherwise the previous track */
  578.     sTrack = cd_curpos.track;
  579.     if (   (0 == cd_curpos.rel_hour) && (0 == cd_curpos.rel_min)
  580.         && (5 > cd_curpos.rel_sec) ) {
  581.     sTrack = cd_curpos.track - 1;
  582.     }
  583.     
  584.     if ( sTrack < 1 ) sTrack = 1;
  585.     eTrack = toc.lasttrack;  /* for now */
  586.     [self playTracks:sTrack to: eTrack];
  587.     
  588.     return self;
  589. }
  590.  
  591. /* you might ask, why does this go thru all the trouble of
  592.  * finding starting and ending times and doing a playaudio_msf
  593.  * instead of just playing a track range?
  594.  *
  595.  * The answer is that I want flexibility such that the CD database
  596.  * can *change* the start and end times for a track, for those
  597.  * CD's which were manufactured with the wrong starting times.
  598. */
  599. - playTracks:(int)startTrack to: (int) endTrack
  600. {
  601.     struct pa_msf tst_start = {0, 0, 0};
  602.     struct pa_msf tst_end = {0, 0, 0};
  603.     int aeTrack;
  604.     int  devReady;
  605.     
  606.     if ( cd_fd == 0 ) return self;    /* no device currently, ignore */
  607.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  608.     if ( devReady )   return self;    /* device is not ready, ignore */
  609.     
  610.     /* calc the track after the end track*/
  611.     if ( endTrack >= toc.lasttrack ) aeTrack = 100;
  612.     else aeTrack = endTrack + 1;
  613.     
  614.     /* note that do_playaudio_msf_47 has no hours variable... */
  615.     tst_start.min = (toc.info[startTrack].hour * 60)
  616.                 + toc.info[startTrack].min;
  617.     tst_start.sec = toc.info[startTrack].sec;
  618.     tst_start.frame = toc.info[startTrack].frame;
  619.  
  620.     tst_end.min = (toc.info[aeTrack].hour * 60) + toc.info[aeTrack].min;
  621.     tst_end.sec = toc.info[aeTrack].sec;
  622.     tst_end.frame = toc.info[aeTrack].frame;
  623.     if ( tst_end.frame > 0 ) tst_end.frame--;
  624.     else {
  625.     tst_end.frame = 74;
  626.         if ( tst_end.sec > 0 ) tst_end.sec--;
  627.     else {
  628.         tst_end.sec = 59;
  629.         if ( tst_end.min > 0 ) tst_end.min--;
  630.         }
  631.     }
  632.     
  633.     if ( [globPrefs consoleDebugMsgs] ) {
  634.     printf("mCD debug: start (%d) %02u:%02u-%02u",
  635.             startTrack,
  636.             tst_start.min, tst_start.sec, tst_start.frame);
  637.     printf("  end (%d) %02u:%02u-%02u\n",
  638.             endTrack,
  639.             tst_end.min, tst_end.sec, tst_end.frame);
  640.     }
  641.     do_playaudio_msf_47(cd_fd, tst_start, tst_end, &cd_Ereply);
  642.  
  643.     /* make sure the volumes are set right (doing a playaudio command
  644.      * causes the CD to play at full volume, even if modesense shows
  645.      * that some other volume level has been set)
  646.      */
  647.     do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
  648.     [leftVolumeSliderID setIntValue: cd_volumes.ch0_vol];
  649.     [rightVolumeSliderID setIntValue: cd_volumes.ch1_vol];
  650.     [self setPlayVolumes:cd_volumes.ch0_vol :cd_volumes.ch1_vol];
  651.  
  652.     /* ensure pause button is set right */
  653.     [pauseButtonID setIntValue:0];
  654.  
  655.     return self;
  656. }
  657.  
  658. - showInfoPanel:sender
  659. {
  660.     if ( ! _mCD_InfoPanel )
  661.     _mCD_InfoPanel = [NXApp loadNibSection:"mCD_Info.nib" owner:self];
  662.     [_mCD_InfoPanel makeKeyAndOrderFront:self];
  663.  
  664.     return self;
  665. }
  666.  
  667. - showTestCD:sender
  668. {
  669.     if ( ! testCD_ID )
  670.     [NXApp loadNibSection:"testCD.nib" owner:self];
  671.     [testCD_ID showUsingPrefs:globPrefs];
  672.  
  673.     return self;
  674. }
  675.  
  676. - stopCD:sender
  677. {
  678.     int  devReady;
  679.     char buff[40];
  680.  
  681.     if ( cd_fd == 0 ) return self;    /* no device yet, ignore */
  682.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  683.     if ( devReady )   return self;    /* device is not ready, ignore */
  684.     
  685.     do_rezerounit_01(cd_fd, &cd_Ereply);
  686.     do_stopunit_1b(cd_fd, &cd_Tval, &cd_Ereply);
  687.     curCdIndex = -1;
  688.     curAbsSecond = -1;
  689.  
  690.     /* probably should reset time-displaying fields differently */
  691.     [trackRemTimeID setStringValue: "-:--:--"];
  692.     if( toc.info[100].min >= 60 ) {
  693.     toc.info[100].hour++ ;
  694.     toc.info[100].min -= 60;
  695.     }
  696.     sprintf (buff, "%u:%02u:%02u", toc.info[100].hour,
  697.             toc.info[100].min, toc.info[100].sec);
  698.     [discRemTimeID setStringValue: buff];
  699.  
  700.     return self;
  701. }
  702.  
  703. - copyMcdEntry:sender
  704. {
  705.     int     track;
  706.     NXStream  *mstream;
  707.     id   pboard;
  708.     NXAtom ptypes[1];
  709.  
  710.     /* note: probably should check that there really is an mCD entry
  711.      *       to copy at this point in time...
  712.      */
  713.     mstream = NXOpenMemory(NULL, 0, NX_READWRITE);
  714.     if( toc.info[100].min >= 60 ) {
  715.     toc.info[100].hour++ ;
  716.     toc.info[100].min -= 60;
  717.     }
  718.     NXPrintf(mstream, "%u:%02u:%02u\t", toc.info[100].hour,
  719.                toc.info[100].min, toc.info[100].sec);
  720.     
  721.     if ( ! toc.discTitle )
  722.     NXPrintf(mstream, "**artist unknown**\t- ** title unknown **");
  723.     else {
  724.     if ( ! toc.discPerformer )
  725.         NXPrintf(mstream, "**artist unknown**");
  726.     else
  727.         NXPrintf(mstream, "%s", toc.discPerformer);
  728.     NXPrintf(mstream, "\t- %s", toc.discTitle);
  729.     }
  730.     if ( toc.discCatNum )
  731.     NXPrintf(mstream, "\tUPC=%s", toc.discCatNum);
  732.     NXPrintf(mstream, "\n");
  733.     
  734.     /* include the table of contents */
  735.     for ( track = toc.firsttrack; track <= toc.lasttrack; track++ ) {
  736.     NXPrintf(mstream, "%4d)\t%02d:%02d\t- ", track,
  737.               toc.info[track].elapsedSec / 60,
  738.               toc.info[track].elapsedSec % 60);
  739.     if ( toc.info[track].trackTitle )
  740.         NXPrintf(mstream, "%s", toc.info[track].trackTitle);
  741.     else    NXPrintf(mstream, "???");
  742.     NXPrintf(mstream, "\n");
  743.     }
  744.     NXPrintf(mstream, "\n");
  745.  
  746.     pboard = [Pasteboard new];
  747.     ptypes[0] = NXAsciiPboardType;
  748.     [pboard declareTypes:ptypes num:1 owner:self];
  749.     [pboard writeType:NXAsciiPboardType fromStream:mstream];
  750.     NXCloseMemory(mstream, NX_FREEBUFFER);
  751.     
  752.     return self;
  753. }
  754.  
  755. - copyMcdEntryAsObjC:sender
  756. {
  757.     u_int key;
  758.     int     track;
  759.     NXStream  *mstream;
  760.     id   pboard;
  761.     NXAtom ptypes[1];
  762.  
  763.     /* note: probably should check that there really is an mCD entry
  764.      *       to copy at this point in time...
  765.      */
  766.     mstream = NXOpenMemory(NULL, 0, NX_READWRITE);
  767.     key = [cd_dbase indexKey:&toc];
  768.     NXPrintf(mstream, "#define %s %10u /* key parts = %d.%02u.%02d.%d %d */\n",
  769.              "0000_none_0000", key, toc.info[100].elapsedSec,
  770.          toc.info[100].frame, toc.lasttrack, toc.info[1].elapsedSec,
  771.          toc.info[toc.lasttrack].elapsedSec);
  772.  
  773.     if( toc.info[100].min >= 60 ) {
  774.     toc.info[100].hour++ ;
  775.     toc.info[100].min -= 60;
  776.     }
  777.     NXPrintf(mstream, "    if ( cdKey == %s ) { /* %u:%02u:%02u-%02u */\n",
  778.              "0000_none_0000", toc.info[100].hour,
  779.                toc.info[100].min, toc.info[100].sec, toc.info[100].frame);
  780.     
  781.     if ( ! toc.discPerformer )
  782.     NXPrintf(mstream, "\ttocPtr->discPerformer = \"performer??\";\n");
  783.     else
  784.     NXPrintf(mstream, "\ttocPtr->discPerformer = \"%s\";\n", toc.discPerformer);
  785.  
  786.     if ( ! toc.discTitle )
  787.     NXPrintf(mstream, "\ttocPtr->discTitle = \"title??\";\n");
  788.     else
  789.     NXPrintf(mstream, "\ttocPtr->discTitle = \"%s\";\n", toc.discTitle);
  790.  
  791.     if ( toc.discCatNum )
  792.     NXPrintf(mstream, "\ttocPtr->discCatNum = \"%s\";\n", toc.discCatNum);
  793.  
  794. #ifdef ADD_SKIPSONGS
  795.     NXPrintf(mstream, "#     if !defined(SKIPSONGS_\n");
  796. #endif
  797.     
  798.     /* include the table of contents */
  799.     for ( track = toc.firsttrack; track <= toc.lasttrack; track++ ) {
  800.     NXPrintf(mstream, "\tSET_CDtt(%2d, %02d.%02d, \"", track,
  801.               toc.info[track].elapsedSec / 60,
  802.               toc.info[track].elapsedSec % 60);
  803.     if ( toc.info[track].trackTitle )
  804.         NXPrintf(mstream, "%s", toc.info[track].trackTitle);
  805.     NXPrintf(mstream, "\");\n");
  806.     }
  807.  
  808.     /* finish off the entry */
  809. #ifdef ADD_SKIPSONGS
  810.     NXPrintf(mstream, "#     endif\n");
  811. #endif
  812.     NXPrintf(mstream, "\t}\n");
  813.  
  814.     pboard = [Pasteboard new];
  815.     ptypes[0] = NXAsciiPboardType;
  816.     [pboard declareTypes:ptypes num:1 owner:self];
  817.     [pboard writeType:NXAsciiPboardType fromStream:mstream];
  818.     NXCloseMemory(mstream, NX_FREEBUFFER);
  819.     
  820.     return self;
  821. }
  822.  
  823. - pasteMcdEntry:sender
  824. {
  825.     return self;
  826. }
  827.  
  828. - (BOOL)openCdFd:(BOOL)tryToLoad
  829. {
  830.     int    devReady;
  831.  
  832.     strcpy(rawDevName, [globPrefs rawDeviceName]);
  833.  
  834.     if ( cd_fd > 0 ) close( cd_fd );
  835.  
  836.     if (tryToLoad == FROM_LOAD_CD_REQUEST) {
  837.     cd_fd = open( rawDevName, O_RDONLY );
  838.     if ( cd_fd < 0 ) {
  839.         NXRunAlertPanel(0,
  840.         "Error return from open() for device %s",
  841.         0, 0, 0, rawDevName);
  842.         cd_fd = 0;
  843.         return NO;
  844.         }
  845.     }
  846.     else {
  847.     cd_fd = open( rawDevName, O_RDONLY | O_NDELAY );
  848.     if ( cd_fd < 0 ) {
  849.         cd_fd = 0;
  850.         return NO;
  851.         }
  852.     }
  853.  
  854.     /* should check the result of do_inquiry to see which model
  855.      * of CD-ROM drive is there (so this would know which SCSI
  856.      * commands can be sent to the drive) */
  857.     do_inquiry(cd_fd, &cd_Inq, &cd_Ereply);
  858.     
  859.     if ( cd_Inq.ir_devicetype != DEVTYPE_CDROM ) {
  860.     if (tryToLoad == FROM_LOAD_CD_REQUEST) {
  861.         NXRunAlertPanel(0,
  862.         "Device %s does not seem to be a CD-ROM drive",
  863.         0, 0, 0, rawDevName);
  864.         }
  865.     else {
  866.         printf("Device %s does not seem to be a CD-ROM drive\n",
  867.              rawDevName);
  868.         }
  869.     close( cd_fd );
  870.     return NO;
  871.     }
  872.  
  873.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  874.     if ( cd_Ereply.er_sensekey == SENSE_UNITATTENTION ) {
  875.     /* drive is signalling attention because there is a new disc
  876.      * in the drive since the last time it was checked.  In this
  877.      * context (loading in a CD), we don't care that it's new
  878.     */
  879.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  880.     }
  881.     if (    (tryToLoad == FROM_LOAD_CD_REQUEST)
  882.          && (devReady == -1)
  883.      && (cd_Ereply.er_sensekey == SENSE_NOSENSE) ) {
  884.     /* I don't understand what the culprit is that forces this
  885.      * special case.  It is needed on my NS/Intel box, but only
  886.      * on the *first* time the "Load" button is selected after
  887.      * the system is rebooted.  For some reason, "loads" at that
  888.      * point will not succeed unless an unload is first done on
  889.      * the device... 
  890.     */
  891.     int err;
  892.     printf("mCD; device not ready, unloading and reloading the CD\n");
  893.     err = ioctl(cd_fd, DKIOCEJECT, NULL);
  894.     close( cd_fd );
  895.     cd_fd = 0;
  896.     cd_fd = open( rawDevName, O_RDONLY );
  897.     if ( cd_fd < 0 ) {
  898.         NXRunAlertPanel(0,
  899.         "Error return from second open() on device %s",
  900.         0, 0, 0, rawDevName);
  901.         cd_fd = 0;
  902.         return NO;
  903.         }
  904.     devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
  905.     }
  906.     if ( devReady ) {
  907.     if (tryToLoad == FROM_LOAD_CD_REQUEST) {
  908.         printf("mCD: Device %s is not ready (%d, %d)\n", rawDevName,
  909.             devReady, cd_Ereply.er_sensekey);
  910.         /* the cd_fd is not freed here, on purpose */
  911.         /* -------
  912.          * Hmm.  I think I'll change my mind on that.  I forget
  913.          * why I didn't want to free it, but now I think I do
  914.          * at least for one case.
  915.          */
  916.         if ((devReady == -1) && (cd_Ereply.er_sensekey == SENSE_NOSENSE)) {
  917.         int err;
  918.         printf("mCD; device not ready, unloading the CD\n");
  919.         err = ioctl(cd_fd, DKIOCEJECT, NULL);
  920.         close( cd_fd );
  921.         cd_fd = 0;
  922.         }
  923.         }
  924.     else {
  925.         /* freeing cd_fd in this case might be a preference option
  926.          * to add someday... */
  927.         close( cd_fd );
  928.         }
  929.     return NO;
  930.     }
  931.  
  932.     if (tryToLoad == FROM_CD_APP_DID_INIT) {
  933.     /* need to think more about under what circumstances the current
  934.      * volumes should be set from global preference values
  935.      */
  936.     int leftInt, rightInt;
  937.     [globPrefs getVolumes:&leftInt:&rightInt];
  938.     cd_volumes.ch0_vol = leftInt;
  939.     cd_volumes.ch1_vol = rightInt;
  940.     [self setPlayVolumes:cd_volumes.ch0_vol :cd_volumes.ch1_vol];
  941.     } else {
  942.     do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
  943.     }
  944.     [leftVolumeSliderID setIntValue: cd_volumes.ch0_vol];
  945.     [rightVolumeSliderID setIntValue: cd_volumes.ch1_vol];
  946.  
  947.     curCdIndex = -1;
  948.     curAbsSecond = -1;
  949.  
  950.     /* do_preventremoval_1e(cd_fd, YES, &cd_Ereply); */
  951.     do_timed_updates = YES;  /* initial testing */
  952.     [loadButtonID setEnabled:NO];
  953.     [unloadButtonID setEnabled:YES];
  954.     return YES;        /* successfully setup */
  955. }
  956.  
  957. - fillTocTitles
  958. {
  959.     u_int    key;
  960.     struct rsc_media_catnum_reply cd_catnum;
  961.     char        origCatNumber[MEDIA_CATNUM_LENGTH];
  962.     BOOL        switchUPC;
  963.  
  964.     key = [cd_dbase indexKey:&toc];
  965.  
  966.     setreuid(-1, geteuid());            /* switch to real uid */
  967.     [cd_dbase fillTocTitles:&toc givenKey:key];
  968.     setreuid(-1, 0);                /* back to being root */
  969.  
  970.     /* check out the media catalog number, if this CD has one */
  971.     switch (cd_curpos.rsc_audio_status) {
  972.     case RSC_ASTAT_PLAYING:
  973.         /* don't read the media catalog number.  Trying to read
  974.          * it will stop the CD on the Apple CD300 and NeXT
  975.          * drives.  [on the Toshiba drive, it doesn't stop the
  976.          * music, and in fact the command sometimes won't work
  977.          * return the catalog number unless the CD *is* playing!]
  978.          */
  979.         if ( !toc.discCatNum && [globPrefs consoleDebugMsgs] ) {
  980.         printf("mCD info: didn't check for media catalog number\n");
  981.         }
  982.         break;
  983.     case RSC_ASTAT_PAUSED:
  984.         /* it'd be nice to take the same precaution when the cd-rom
  985.          * drive is paused, but it seems that the CD-ROM drive that
  986.          * NeXT used to sell (a Sony mechanism) returns "paused" at
  987.          * times when it's not really paused.  The Apple CD300 (a
  988.          * newer Sony mechanism) doesn't seem to have this problem.
  989.          */
  990.     case RSC_ASTAT_PLAYCOMPLETE:
  991.     case RSC_ASTAT_PLAYABORTED:
  992.     case RSC_ASTAT_NONE:
  993.         switchUPC = NO;
  994.         do_readmediacatnum_42(cd_fd, &cd_catnum, &cd_Ereply);
  995.         if ( cd_catnum.catnum_isSet ) {
  996.         memcpy(origCatNumber, cd_catnum.media_catnum, MEDIA_CATNUM_LENGTH);
  997.         fixCatNum(&cd_catnum);
  998.         if (!toc.discCatNum) switchUPC = YES;
  999.         else {
  1000.             if ( strncmp(toc.discCatNum, cd_catnum.media_catnum, MEDIA_CATNUM_LENGTH)) {
  1001.             /* the UPC in our database does not match the UPC this
  1002.              * drive found on the CD itself.  First have to check
  1003.              * for lame NeXT-CD-ROM case (which misses 3 chars)
  1004.              */
  1005.             if ( cd_catnum.media_catnum[MEDIA_CATNUM_LENGTH-1] != '?') switchUPC = YES;
  1006.             else {
  1007.                 if ( strncmp(toc.discCatNum,cd_catnum.media_catnum,
  1008.                         MEDIA_CATNUM_LENGTH-3)) switchUPC = YES;
  1009.                 }
  1010.             }
  1011.             if ( switchUPC && [globPrefs consoleDebugMsgs]) {
  1012.             /* tell the user of mismatch */
  1013.             int    i;
  1014.             u_char    *cptr; 
  1015.             printf("mCD info: original UPC = x'");
  1016.             cptr = &(origCatNumber[0]);
  1017.             printf("%.2X", *cptr++);  /* ignore comp. warning */
  1018.             for (i=1; i<MEDIA_CATNUM_LENGTH; i++) {
  1019.                 printf(" %.2X", *cptr++);  /* ignore comp. warning */
  1020.                 }
  1021.             printf("'\n");
  1022.             printf("mCD info: fixed up UPC =  \"%.15s\"\n", cd_catnum.media_catnum);
  1023.             printf("mCD info: DBase return =  \"%15s\"\n", toc.discCatNum);
  1024.             printf("mCD info: currently using the fixed upCD's catalog number\n");
  1025.             }
  1026.             }
  1027.         }
  1028.         if ( switchUPC ) {
  1029.         memcpy(holdCatNumber, cd_catnum.media_catnum, MEDIA_CATNUM_LENGTH);
  1030.         holdCatNumber[MEDIA_CATNUM_LENGTH+1] = '\0';
  1031.         toc.discCatNum = &(holdCatNumber[0]);
  1032.         }
  1033.         /* Redoing the "spinup" here seems to reduce the problem
  1034.          * of getting a unit attn when do_readmediacatnum finds
  1035.          * a media code on the music CD */
  1036.         do_spinup_1b(cd_fd, &cd_Tval, &cd_Ereply);
  1037.         break;
  1038.     }
  1039.  
  1040.     return self;
  1041. }
  1042.  
  1043.  
  1044. /* these next few are for the benefit of the testCD module,
  1045.  * to implement the protocol testCDstarter */
  1046. - (int) fdOfCdrom  { return cd_fd; }
  1047. - (struct esense_reply*) esenseReplyPtr { return &cd_Ereply; }
  1048. - (struct esense_reply*) esenseTestUnitPtr { return &tur_Ereply; }
  1049. - (struct esense_reply*) esenseCurPosPtr { return &rcp_Ereply; }
  1050.  
  1051. @end
  1052.  
  1053. /* I've been testing this program on three different models of CD-ROM
  1054.  * drives.  Each one returns the catalog number in a different format.
  1055.  * In the case of the CD-ROM drive NeXT used to sell, it doesn't even
  1056.  * return the whole number.  Do the best we can to map the three
  1057.  * formats (that I know of so far) into a common format.
  1058.  *
  1059.  *   Apple CD300 UPC = '3030 3735 3939 3235 3739 3432 3130 30'
  1060.  *   DEC-Toshiba UPC = '0000 0705 0909 0205 0709 0402 0100 00'
  1061.  *   NeXT CD-ROM UPC = '0075 9925 7942 0000 0000 0000 0000 00'
  1062.  *
  1063.  * Not sure if this routine should really be in the scsi_cd subproj...
  1064.  */
  1065. void fixCatNum(catNumPtr)
  1066. struct rsc_media_catnum_reply *catNumPtr;
  1067. {
  1068.     char *tempPtr, *endPtr;
  1069.     BOOL all_char, all_dec;
  1070.  
  1071.     all_char = YES; /* all bytes are characters '0' to '9' */
  1072.     all_dec = YES;  /* all bytes are 0x00 to 0x09 */
  1073.     tempPtr = &(catNumPtr->media_catnum[0]);
  1074.     endPtr = tempPtr + MEDIA_CATNUM_LENGTH;
  1075.     for (; tempPtr < endPtr; tempPtr++) {
  1076.     if (*tempPtr < '0' || *tempPtr > '9') all_char = NO;
  1077.     if (*tempPtr > 0x09) all_dec = NO;
  1078.     }
  1079.     if (all_dec) {
  1080.     /* all decimal, change them to all-characters */
  1081.     tempPtr = catNumPtr->media_catnum;
  1082.     for (; tempPtr < endPtr; tempPtr++) *tempPtr += 0x30;
  1083.     all_dec = NO;
  1084.     all_char = YES;
  1085.     }
  1086.     if (all_char) return;
  1087.     
  1088.     /* the NeXT format, have to expand it, going right-to-left */
  1089.     /* [done the brute-force way, and assumes MEDIA_CATNUM_LENGTH = 15] */
  1090.     catNumPtr->media_catnum[14] = (catNumPtr->media_catnum[7] >> 4) + 0x30;
  1091.     catNumPtr->media_catnum[13] = (catNumPtr->media_catnum[6] & 0x0F) + 0x30;
  1092.     catNumPtr->media_catnum[12] = (catNumPtr->media_catnum[6] >> 4) + 0x30;
  1093.     catNumPtr->media_catnum[11] = (catNumPtr->media_catnum[5] & 0x0F) + 0x30;
  1094.     catNumPtr->media_catnum[10] = (catNumPtr->media_catnum[5] >> 4) + 0x30;
  1095.     catNumPtr->media_catnum[9] = (catNumPtr->media_catnum[4] & 0x0F) + 0x30;
  1096.     catNumPtr->media_catnum[8] = (catNumPtr->media_catnum[4] >> 4) + 0x30;
  1097.     catNumPtr->media_catnum[7] = (catNumPtr->media_catnum[3] & 0x0F) + 0x30;
  1098.     catNumPtr->media_catnum[6] = (catNumPtr->media_catnum[3] >> 4) + 0x30;
  1099.     catNumPtr->media_catnum[5] = (catNumPtr->media_catnum[2] & 0x0F) + 0x30;
  1100.     catNumPtr->media_catnum[4] = (catNumPtr->media_catnum[2] >> 4) + 0x30;
  1101.     catNumPtr->media_catnum[3] = (catNumPtr->media_catnum[1] & 0x0F) + 0x30;
  1102.     catNumPtr->media_catnum[2] = (catNumPtr->media_catnum[1] >> 4) + 0x30;
  1103.     catNumPtr->media_catnum[1] = (catNumPtr->media_catnum[0] & 0x0F) + 0x30;
  1104.     catNumPtr->media_catnum[0] = (catNumPtr->media_catnum[0] >> 4) + 0x30;
  1105.     /* and after all that, we probably have three zeros at the end,
  1106.      * which most-likely means we've lost the last three digits.
  1107.      * Put in question-marks, I guess.
  1108.      */
  1109.    if (   (catNumPtr->media_catnum[14] == '0')
  1110.        && (catNumPtr->media_catnum[13] == '0')
  1111.        && (catNumPtr->media_catnum[12] == '0') ) {
  1112.        catNumPtr->media_catnum[12] = '?';
  1113.        catNumPtr->media_catnum[13] = '?';
  1114.        catNumPtr->media_catnum[14] = '?';
  1115.    }
  1116. }
  1117.